import { exec } from "child_process";
import { DeepPartial } from "@ap/types/util";
import fs from "fs/promises";
import * as jsonc from "jsonc-parser";
import path from "path";

const chatjptViewSleepMs = 500;
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Insert a string into another string at a specific index.
 */
function insertString(
  originalStr: string,
  insertStr: string,
  index: number,
): string {
  const start = originalStr.slice(0, index);
  const end = originalStr.slice(index);
  return `${start}${insertStr}${end}`;
}

/**
 * Concatenate a string or array of strings with another string or array of strings.
 * @param originalStr The original string or array of strings.
 * @param args The string or array of strings to concatenate.
 * @returns The concatenated string or array of strings.
 * @example
 * concatString('Hello', ' World'); // 'Hello World'
 * concatString(['Hello', 'World'], ' '); // 'Hello World'
 * concatString('Hello', [' ', 'World']); // 'Hello World'
 * concatString(['Hello', 'World'], [' ', ' ']); // 'Hello World '
 * concatString(['Hello', 'World'], [' ', 'World']); // 'Hello World World'
 * concatString(['Hello', 'World'], [' ', 'my ', '!']); // 'Hello my World!'
 */
function concatString(originalStr: string, args: string | string[]): string;
function concatString(originalStr: string[], args: string): string;
function concatString(originalStr: string[], args: string[]): string;
function concatString(
  originalStr: string | string[],
  args: string | string[],
): string {
  if (Array.isArray(originalStr)) {
    if (Array.isArray(args)) {
      // 当 originalStr 和 args 都是数组时，交替连接它们的元素
      let output = [];
      for (let i = 0; i < originalStr.length; i++) {
        output.push(originalStr[i]);
        if (i < args.length) {
          output.push(args[i]);
        }
      }
      return output.join("");
    } else {
      // 当 originalStr 是数组而 args 是字符串时，在第一个数组元素后连接 args 字符串
      return [originalStr[0], args, ...originalStr.slice(1)].join("");
    }
  } else {
    if (Array.isArray(args)) {
      // 当 originalStr 是字符串而 args 是数组时，将 originalStr 与 args 数组连接
      return [originalStr, ...args].join("");
    } else {
      // 当 originalStr 和 args 都是字符串时，直接连接它们
      return originalStr + args;
    }
  }
}

/**
 * 合并两个对象
 * ? 这个函数可能会导致性能问题
 */
function mergeObject<T extends Record<string, any>>(
  originalObj: DeepPartial<T>,
  newObj: T,
): T {
  const obj = JSON.parse(JSON.stringify(originalObj));
  for (const key in newObj) {
    if (
      (newObj[key] as any) instanceof Object &&
      (newObj[key] as any) !== null
    ) {
      obj[key] = mergeObject(obj[key], newObj[key] as any);
    } else {
      obj[key] = newObj[key];
    }
  }
  return obj;
}

async function tasks<T>(fns: (() => Promise<T>)[], delay = 1000): Promise<T[]> {
  const res: T[] = [];
  for (const fn of fns) {
    res.push(await fn());
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
  return res;
}

async function isInstalledInNpm(name: string, cwd?: string): Promise<boolean> {
  return new Promise<boolean>((resolve) => {
    exec(
      `npm list ${name}`,
      {
        cwd,
      },
      (_, stdout) => {
        if (stdout.includes(name)) {
          resolve(true);
        } else {
          resolve(false);
        }
      },
    );
  });
}
async function isAllInstalledInNpm(
  names: string[],
  cwd?: string,
): Promise<boolean> {
  return new Promise<boolean>(async (resolve) => {
    const list = await new Promise<string>((resolve) => {
      exec(
        `npm list`,
        {
          cwd,
        },
        (_, stdout) => {
          resolve(stdout);
        },
      );
    });
    for (const name of names) {
      if (!list.includes(name)) {
        resolve(false);
      }
    }
    resolve(true);
  });
}
async function npmInstall(name: string, cwd?: string): Promise<void> {
  return new Promise<void>((resolve) => {
    exec(
      `npm install ${name}`,
      {
        cwd,
      },
      (_, stdout) => {
        console.log(stdout);
        resolve();
      },
    );
  });
}
async function parseJSON<T, U>(json: string, defaultValue: U): Promise<T | U> {
  try {
    return jsonc.parse(json);
  } catch (e) {
    return defaultValue;
  }
}

function tryRun<T, U>(
  fn: () => Promise<T>,
  defaultValue: U,
): () => Promise<T | U>;
function tryRun<T, U>(
  fn: () => Promise<T>,
  defaultValue: (e: Error) => U,
): () => Promise<T | U>;
function tryRun<T, U>(
  fn: () => Promise<T>,
  defaultValue: U | ((e: Error) => U),
): () => Promise<T | U> {
  return async () => {
    try {
      return await fn();
    } catch (e: any) {
      if (typeof defaultValue === "function") {
        return (defaultValue as Function)(e as Error);
      } else {
        return defaultValue;
      }
    }
  };
}

class Logger {
  static Levels = {
    DEBUG: "DEBUG",
    INFO: "INFO",
    WARN: "WARN",
    ERROR: "ERROR",
  };
  constructor(
    private prefix: string,
    private filepath: string,
  ) {}
  private generate(level: string, message: string): string {
    const time = new Date().toLocaleString();
    return `${time} ${this.prefix}: [${level}] ${message}`;
  }
  async createIfNotExist(): Promise<void> {
    try {
      await fs.access(this.filepath);
    } catch {
      await fs.writeFile(this.filepath, "");
    }
  }
  async debug(message: string): Promise<void> {
    await this.createIfNotExist();
    return await fs.appendFile(
      this.filepath,
      this.generate(Logger.Levels.DEBUG, message) + "\n",
    );
  }
  async info(message: string): Promise<void> {
    await this.createIfNotExist();
    return await fs.appendFile(
      this.filepath,
      this.generate(Logger.Levels.INFO, message) + "\n",
    );
  }
  async warn(message: string): Promise<void> {
    await this.createIfNotExist();
    return await fs.appendFile(
      this.filepath,
      this.generate(Logger.Levels.WARN, message) + "\n",
    );
  }
  async error(message: string): Promise<void> {
    await this.createIfNotExist();
    return await fs.appendFile(
      this.filepath,
      this.generate(Logger.Levels.ERROR, message) + "\n",
    );
  }
  async log(level: string, message: string): Promise<void> {
    await this.createIfNotExist();
    return await fs.appendFile(
      this.filepath,
      this.generate(level, message) + "\n",
    );
  }
}

function resolveIfNotAbsolue(p: string, base: string): string {
  return path.isAbsolute(p) ? p : path.resolve(base, p);
}

async function safeReadJSONC(p: string): Promise<Record<string, any>> {
  const file = await fs.readFile(p, "utf-8");
  return jsonc.parse(file);
}

async function parallel<T, U>(
  fns: (() => Promise<U>)[],
  par: boolean,
): Promise<T> {
  if (par) {
    return (await Promise.all(fns.map((fn) => fn()))) as T;
  } else {
    return (await tasks(fns, 0)) as T;
  }
}

function nowLog(date: Date): string {
  // 用于输出日志的时间格式
  // 例如2024-07-18 23:19:08.269
  return date.toISOString().replace("T", " ").replace("Z", "");
}

type Task<T> = () => Promise<T>;

function queue<T>(
  fc: (...args: any[]) => Promise<T>,
): (...args: any[]) => Promise<T> {
  let lastTask: Promise<any> = Promise.resolve();
  let taskQueue: Task<T>[] = [];
  let currentTask: Promise<T> | null = null;
  let pendingResolves: ((value: T) => void)[] = [];
  let pendingRejects: ((reason?: any) => void)[] = [];
  let isTaskRunning = false;

  return () => {
    return new Promise<T>((resolve, reject) => {
      if (isTaskRunning) {
        resolve(Promise.resolve() as unknown as T);
        return;
      }

      const task: Task<T> = () => {
        return new Promise<T>((taskResolve, taskReject) => {
          try {
            const result = fc();
            taskResolve(result);
          } catch (error) {
            taskReject(error);
          }
        });
      };

      taskQueue.push(task);
      pendingResolves.push(resolve);
      pendingRejects.push(reject);

      const runNextTask = () => {
        if (taskQueue.length === 0) {
          currentTask = null;
          isTaskRunning = false;
          return;
        }

        const nextTask = taskQueue.shift()!;
        currentTask = nextTask()
          .then((result) => {
            pendingResolves.forEach((res) => res(result));
            pendingResolves = [];
            return result;
          })
          .catch((error) => {
            pendingRejects.forEach((rej) => rej(error));
            pendingRejects = [];
            throw error;
          })
          .finally(runNextTask);
      };

      if (!currentTask) {
        isTaskRunning = true;
        runNextTask();
      }
    });
  };
}

export { queue, Task };

export {
  Logger,
  insertString,
  concatString,
  mergeObject,
  tasks,
  isInstalledInNpm,
  isAllInstalledInNpm,
  npmInstall,
  parseJSON,
  tryRun,
  resolveIfNotAbsolue,
  safeReadJSONC,
  parallel,
  nowLog,
  sleep,
  chatjptViewSleepMs,
};
